﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using Windows.Storage;
using System.Reflection;
using System.Collections;
using System.Collections.ObjectModel;

namespace Windows8Accelerator.Core.Xaml.Serialization
{
    public static class XmlSerializer
    {
        private static List<Type> GetKnownTypesByObject(object obj)
        {
            var types = new Dictionary<Type, Type>();
            var visitedObjects = new List<object>();
            AddKnownTypesByObject(obj, types, visitedObjects);
            
            var arrayItemTypes = new List<Type>();
            var result = new List<Type>();
            foreach (var type in types.Keys)
            {
                if ((type.Namespace.StartsWith("System") ||
                    type.Namespace.StartsWith("Windows") ||
                    type == typeof(string)) &&
                    (!type.FullName.StartsWith("System.Collections.Generic.List") &&
                    !type.FullName.StartsWith("System.Collections.Generic.Dictionary")))
                    continue;

                Type arrayItemType = null;
                if (type.IsArray)
                    arrayItemType = type.GetElementType();
                var typeInfo = type.GetTypeInfo();
                if (typeInfo.IsGenericType)
                {
                    var genericTypeDefinition = typeInfo.GetGenericTypeDefinition();
                    if (genericTypeDefinition == typeof(List<>) ||
                        genericTypeDefinition == typeof(ObservableCollection<>))
                    {
                        arrayItemType = typeInfo.GenericTypeArguments[0];
                    }
                }

                if (arrayItemType != null)
                {
                    if (!arrayItemTypes.Contains(arrayItemType))
                    {
                        arrayItemTypes.Add(arrayItemType);
                        result.Add(type);
                    }
                }
                else
                {
                    result.Add(type);
                }
            }
            return result;
        }

        private static Dictionary<Type, List<Type>> KnownTypesDictionary = new Dictionary<Type, List<Type>>();

        private static List<Type> GetKnownTypesByType(Type type)
        {
            if (!KnownTypesDictionary.ContainsKey(type))
            {
                var types = new Dictionary<Type, Type>();
                AddKnownTypesByType(type, types);
                KnownTypesDictionary[type] = types.Keys.ToList();
            }
            return KnownTypesDictionary[type];
        }

        private static void AddKnownTypesByObject(object obj, Dictionary<Type, Type> types, List<object> visitedObjects)
        {   
            if (obj == null)
                return;
            if (visitedObjects.Contains(obj))
                return;
            visitedObjects.Add(obj);
            var type = obj.GetType();
            var typeInfo = type.GetTypeInfo();
            if (IsSystemType(type))
                return;
            AddType(types, type);
            foreach (var field in typeInfo.DeclaredFields)
            {
                try
                {
                    if (field.IsPublic && !field.IsStatic && CanSerialize(field))
                    {
                        var value = field.GetValue(obj);
                        AddKnownTypesByObject(value, types, visitedObjects);
                    }
                }
                catch { }
            }
            foreach (var property in typeInfo.DeclaredProperties)
            {
                try
                {
                    if (property.CanRead && !property.GetMethod.IsStatic && property.GetMethod.IsPublic && CanSerialize(property) && property.GetIndexParameters().Length==0)
                    {
                        var value = property.GetValue(obj);
                        AddKnownTypesByObject(value, types, visitedObjects);
                    }
                }
                catch { }
            }

            if (obj is IDictionary)
            {
                foreach (var item in ((IDictionary)obj).Keys)
                    AddKnownTypesByObject(item, types, visitedObjects);
                foreach (var item in ((IDictionary)obj).Values)
                    AddKnownTypesByObject(item, types, visitedObjects);
            }
            else if (obj is IEnumerable)
            {
                foreach (var item in (IEnumerable)obj)
                {
                    AddKnownTypesByObject(item, types, visitedObjects);
                }
            }
        }

        private static void AddType(Dictionary<Type, Type> types, Type type)
        {
            types[type] = type;
        }

        private static bool CanSerialize(MemberInfo memberInfo)
        {
            return memberInfo.GetCustomAttribute(typeof(IgnoreDataMemberAttribute)) == null;
        }

        private static void AddKnownTypesByType(Type type, Dictionary<Type, Type> types)
        {
            var typeInfo = type.GetTypeInfo();
            if (IsSystemType(type) || types.ContainsKey(type))
                return;
            AddType(types, type);
            foreach (var field in typeInfo.DeclaredFields)
            {
                try
                {
                    if (field.IsPublic && !field.IsStatic && CanSerialize(field))
                    {
                        AddKnownTypesByType(field.FieldType, types);
                    }
                }
                catch { }
            }
            foreach (var property in typeInfo.DeclaredProperties)
            {
                try
                {
                    if (property.CanRead && !property.GetMethod.IsStatic && property.GetMethod.IsPublic && CanSerialize(property) && property.GetIndexParameters().Length == 0)
                    {
                        AddKnownTypesByType(property.PropertyType, types);
                    }
                }
                catch { }
            }
        }

        private static bool IsSystemType(Type type)
        {
            return type == typeof(string) || type.GetTypeInfo().IsPrimitive; 
        }

        public static void Serialize(Stream stream, object obj)
        {
            var typeList = GetKnownTypesByObject(obj);
            var settings = new DataContractSerializerSettings();
            settings.KnownTypes = typeList;
            var serializer = new DataContractSerializer(obj.GetType(), settings);
            serializer.WriteObject(stream, obj);
        }

        public static T Deserialize<T>(Stream stream)
        {
            var settings = new DataContractSerializerSettings();
            settings.KnownTypes = GetKnownTypesByType(typeof(T));
            var serializer = new DataContractSerializer(typeof(T), settings);
            return (T)(serializer.ReadObject(stream));
        }
    }

}
